Skip to content

Commit 5e3a68c

Browse files
committed
properly handle revision based_on field
1 parent b2b7ce4 commit 5e3a68c

File tree

4 files changed

+108
-2
lines changed

4 files changed

+108
-2
lines changed

kitsune/wiki/handlers.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,24 @@ def on_user_deletion(self, user: User) -> None:
3030
Revision.objects.filter(readied_for_localization_by=user).update(
3131
readied_for_localization_by=sumo_bot
3232
)
33+
# The based-on value of localized revisions is essential, so if the based-on revision
34+
# is un-approved and created by the user to be deleted, we should ensure that it is
35+
# retained by re-assigning it to the "sumo_bot". It's very rare that the based-on
36+
# revision of a localized revision is un-approved (less than 1%), but it is possible.
37+
# The based-on value of non-localized revisions is not nearly as important, so in those
38+
# cases, we can simply let the cascade behavior set the based-on value to NULL.
39+
Revision.objects.filter(
40+
id__in=(
41+
Revision.objects.exclude(document__locale=settings.WIKI_DEFAULT_LANGUAGE)
42+
.filter(
43+
based_on__creator=user,
44+
based_on__is_approved=False,
45+
# This locale check ensures only legitimate based-on references.
46+
based_on__document__locale=settings.WIKI_DEFAULT_LANGUAGE,
47+
)
48+
.values_list("based_on__id", flat=True)
49+
)
50+
).update(creator=sumo_bot)
3351

3452
documents = Document.objects.filter(contributors=user)
3553
for document in documents:
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Generated by Django 4.2.20 on 2025-03-21 08:59
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("wiki", "0018_alter_document_restrict_to_groups"),
11+
]
12+
13+
operations = [
14+
migrations.AlterField(
15+
model_name="revision",
16+
name="based_on",
17+
field=models.ForeignKey(
18+
blank=True,
19+
null=True,
20+
on_delete=django.db.models.deletion.SET_NULL,
21+
to="wiki.revision",
22+
),
23+
),
24+
]

kitsune/wiki/models.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -836,7 +836,7 @@ class Revision(ModelBase, AbstractRevision):
836836
# Edit button was hit to begin creating this revision. If there was none,
837837
# this is simply the latest of the default locale's revs as of that time.
838838
# Used to determine whether localizations are out of date.
839-
based_on = models.ForeignKey("self", on_delete=models.CASCADE, null=True, blank=True)
839+
based_on = models.ForeignKey("self", on_delete=models.SET_NULL, null=True, blank=True)
840840
# TODO: limit_choices_to={'document__locale':
841841
# settings.WIKI_DEFAULT_LANGUAGE} is a start but not sufficient.
842842

@@ -965,7 +965,6 @@ def latest_revision(excluded_rev, constraint):
965965
except IndexError:
966966
return None
967967

968-
Revision.objects.filter(based_on=self).update(based_on=None)
969968
document = self.document
970969

971970
# If the current_revision is being deleted, try to update it to the

kitsune/wiki/tests/test_handlers.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,68 @@ def test_mixed_revisions_user_deletion(self):
205205

206206
rev2 = Revision.objects.filter(id=rev2_id).first()
207207
self.assertIsNone(rev2, "Deleted user's revision should be deleted")
208+
209+
def test_revision_based_on_deletion(self):
210+
"""
211+
Test the handling of a revision's "based_on" field when it references a revision
212+
created by a user that is deleted.
213+
"""
214+
doc = DocumentFactory()
215+
rev1 = RevisionFactory(document=doc, creator=self.user)
216+
rev2 = RevisionFactory(document=doc, based_on=rev1)
217+
rev3 = ApprovedRevisionFactory(document=doc, based_on=rev2)
218+
219+
doc_id = doc.id
220+
rev1_id = rev1.id
221+
rev2_id = rev2.id
222+
rev3_id = rev3.id
223+
224+
self.listener.on_user_deletion(self.user)
225+
226+
self.assertTrue(
227+
Document.objects.filter(id=doc_id).exists(), "Document should not be deleted"
228+
)
229+
self.assertFalse(
230+
Revision.objects.filter(id=rev1_id).exists(),
231+
"Deleted user's unapproved revision should be deleted",
232+
)
233+
rev2 = Revision.objects.filter(id=rev2_id).first()
234+
self.assertIsNotNone(rev2, "Other user's revision should not be deleted")
235+
self.assertIsNone(rev2.based_on, "Other user's revision's based_on should now be None")
236+
rev3 = Revision.objects.filter(id=rev3_id).first()
237+
self.assertIsNotNone(rev3, "The approved revision should not be deleted")
238+
self.assertEqual(rev3.based_on, rev2)
239+
240+
it_doc = DocumentFactory(locale="it", parent=doc)
241+
it_rev = RevisionFactory(document=it_doc, based_on=rev2)
242+
243+
it_doc_id = it_doc.id
244+
it_rev_id = it_rev.id
245+
246+
self.listener.on_user_deletion(rev2.creator)
247+
248+
# Ensure that the default document wasn't deleted.
249+
self.assertTrue(
250+
Document.objects.filter(id=doc_id).exists(), "Document should not be deleted"
251+
)
252+
253+
# The deleted user's un-approved revision should not have been deleted,
254+
# but instead re-assigned to the sumo bot, because it's the based-on
255+
# value of a localized revision.
256+
rev2 = Revision.objects.filter(id=rev2_id).first()
257+
self.assertIsNotNone(rev2, "Deleted user's revision should not be deleted")
258+
self.assertEqual(rev2.creator.username, settings.SUMO_BOT_USERNAME)
259+
260+
# Ensure that the third revision, whose based-on value is the revision
261+
# whose creator was deleted, was not affected at all.
262+
rev3 = Revision.objects.filter(id=rev3_id).first()
263+
self.assertIsNotNone(rev3, "The approved revision should not be deleted")
264+
self.assertEqual(rev3.based_on, rev2)
265+
266+
self.assertTrue(
267+
Document.objects.filter(id=it_doc_id).exists(),
268+
"The Italian document should not be deleted",
269+
)
270+
it_rev = Revision.objects.filter(id=it_rev_id).first()
271+
self.assertIsNotNone(it_rev, "The Italian revision should not be deleted")
272+
self.assertEqual(it_rev.based_on, rev2)

0 commit comments

Comments
 (0)